Arc started 2026-07-22, after Phase 6. Sections are Roman numerals and steps within them are lettered; each step's title says what it does, followed by a sentence on what was proposed and, once worked, a few on what was done and anything learned. Marker legend: ▶ being worked on, ⚑ implemented and waiting to be reviewed, ✓ done, ✓✗ closed by deciding not to do it, ? open decision, unmarked = planned.
The wiki is where a group keeps its shared pages, so its editing and resource surfaces are what a group touches most. This arc finishes the page-resource folder UI (I), gives the editor a live preview and then a markup toolbar (II), makes the category mechanism a real content tool (III), completes the email witness flow for secret ballots (IV), and documents the folder UI once it is finished (V).
To-do item 17. Most of it landed in Phase 2.5; two pieces stayed open.
Rows are picked in the browser and nothing is sent until an action is
asked for, when the whole set goes as one request. That needed the
server widened: SocialComponent now answers
delete_resources, move_resources,
clip_copy_resources and clip_cut_resources,
each taking a list. Dragging carries the picked set to either drop
target, dimming the rows it carries and showing a count.
in-width table with
repeat(auto-fill, minmax(min, 1fr)) and a container query so
the folder reflows to the width it is given.
table-layout: fixed and four hard column widths gave the
list a 502px floor that overflowed a phone's folder box by 156px; the
list is now a grid whose rows take the table's tracks through
subgrid, and the cards are auto-fill so they
need no column count at all. .wiki-resources is the query
container, so narrowing the folder with the window left wide is what
drops columns, which the old viewport media query could not express.
Measured: list content 502px to 344px at a 360px window, cards 167px to
282px in a 576px folder, desktop unchanged.
To-do items 19 and 20. The renderer in both is the existing
WikiParser, so neither adds a client-side parser.
WikiParser.
The rendering happens in the browser through the port of
WikiParser already in help.js, so a keystroke
costs no request; it waits for a pause in typing, and nothing is
rendered at all until the book beside the way into the page's history
opens the pane, which starts closed and leaves the source the whole
width until it does; the book is shut while the preview is out of
sight and open while it is being read. Source and preview share one window of a settled
height, split by the existing split-adjuster from
wiki.js, and the editor's row of controls moves above both
columns as the preview opens so the two start at the same place and end
level; either can be given more room by dragging, each is put at the other's place in the document as it is
scrolled. Under 52rem the pair becomes one column with the preview
above the source, and a markdown group gets no pane since only the
mediawiki engine is ported. Picking out words in either pane shows them
in the other: the two are matched on their letters and digits alone,
since markup is in one and not the other, and the answer is carried
back to where those letters sit in each. A resource written out as its
own data draws in the preview, since everything needed is in the
markup.
{{category|...}} and {{category-list|...}} into
a real content tool: named parameters, style presets, pagination,
auto-stub index pages, a "Filed under" affordance, cross-group lookup,
and semantic HTML in place of the custom tags. To-do item 18.
Proposed.{{secret-ballot|w1|w2|...}} so named witnesses take
part by email rather than having to be present at once. To-do item 21.
Proposed..txt, not edited into the autogenerated help pages. Best
done after I. To-do item 37. Proposed.I, II, III and IV are independent. Within II the preview comes before the toolbar; V waits on I so it describes finished behavior.
Oldest first.
Git requests now run on the cooperative fiber path and the build
suspends periodically. The earlier attempt to pace the send with
stream() was aimed at the wrong layer and, by dropping
webExit, let the group controller render onto the reply; it
was undone here.
A round with no depth and no "done" now returns just the acknowledgment, and skips the history walk since that round asks for no objects. A five-commit client sends every have and "done" at once, which is why the trouble looked size-dependent.
The server now offers multi_ack_detailed, naming
recognized commits as common and closing the round with
NAK; a flush there is read as a missing acknowledgment and
no closing line leaves the client waiting. The reply is written to a
stream with a running checksum. A seven-hundred-commit pull settles in
two rounds, and 17.7MB is built with 4.0MB of memory against 21.7MB as a
string.
completeDeferredResponse sends only the
buffered body, so a git request on the fiber path sent headers claiming
eighty megabytes and then nothing.
GitUploadPack now yields its reply piece by piece and
serveUploadPack hands that to the ordinary streaming path.
The fiber breathing and its interval constant went with it.
content-length: 0 and an empty body, since the length is
taken from the buffered body, which a streamed reply leaves empty.
The yielded pieces are written to a temporary file, its size is the stated length, and the file is streamed back and removed. Building before sending means a very large clone occupies the server while its packfile is made.
have lines stopped the commit walk but
nothing was set against what the client holds, so a one-commit update
packed a copy of the whole working tree.
The walk now gathers everything the client's known commits record and skips it. A client one commit behind was sent 63 objects and 17.6MB and is now sent 3 objects and 1,755 bytes.
GitDelta writes and reads Git's delta
format, working on bytes so binary content is handled too.
Utility.php's diff and
computeLCS were considered and not used: they compare lines
and build a table capped at a quarter of a million cells.
GitPackReader's private applier moved into
GitDelta, leaving one implementation. A clone of a
forty-commit history of a large file fell from 898,559 to 105,915
bytes.
The server now offers thin-pack and remembers the version
of each file the client holds, sending a new version as its difference
from that. A one-line edit fell from 22,508 bytes to 331.
Not the link: the same server sent a 142MB file from the repository to
the same machine complete, so what differs is how the two are sent, and
the packfile now goes out through readfile as that file
does. The earlier change to the idle sweep, which treats a connection
with a reply still to send as active, stays, though it was never tied to
this. H3Listener does not share that defect, its idle check
counting outbound packets.
The memory line now also reports about how many bytes the kept sessions hold, from a sample of twenty, and how many sit in connection buffers. Sessions are capped by count rather than weight, so their number alone did not say.
A connection's unread input was allowed 256MB on a 1GB limit, half of memory while the buffer grows, so it is now 32MB, one message plus its command. A stored message was read whole however large the file had become, so its size is checked first, on the storage every backend shares. The loop now records which IMAP command it is running, so the next crash line names it.
getPageInfoByName answers false, reading
PAGE off it warns and yields null, and that null reached
preg_match.
All three now check that what came back is an array, in both places the media path does this, and the sign-in check treats a missing page as an empty one.
min-width: 66rem on .body-container meant the
content column could never be narrower than 1056px however narrow the
window was, and the phone layout does not take over until 600px.
That width and the floor under it are now held to the window as well as to their design, and seven more floors underneath doing the same thing give way to their container the same way. Measured on two pages at ten widths, the document's scroll width now equals the window at every width from 601 up, where it had been stuck at 828 and 933. It fed back into I.B, where the folder can now take widths that left one stretched card per row.
A submission passes a backoff counted apart from the sign-in forms, a decoy field, a timing check, stateless proof of work and a rolling daily allowance, and is told in six words or fewer what became of it. Testing found the proof of work replaced the string an answer was answering before reading the answer, and kept it in a session a cookieless visitor does not have, so it now takes string and time back from the form. A refusal names its reason except where naming it would tell a script what to fix.
createGitIssue only on acceptance.
An accepted issue shows the handle where the reporter column would
otherwise show a username.
Rows are selected the way rows are selected elsewhere and decided together, and rejecting runs through one control whose catch also shuts the reporter out for a week, a month or for good, with a searchable paged list of who is shut out and a control to let them back in. Both lists page through one query narrowing on page name and record text, so only the rows shown are opened, and each list is read by scrolling: reaching the end asks for the next page and the rows arrive into the table, the way a feed reads. A waiting report and a ban are each a page beside the repository, told apart from a numbered issue by the mark after its name, which needed a one-off rename of existing issue pages.
It is a field in the repository page's own settings rather than a
group setting, which keeps the whole feature clear of any schema
change. validateHashCode moved from
RegisterController up to Controller so the
account forms and this one ask the same question through one copy of
it.
It now takes only rows of the list itself, and an answer carrying none of them ends the paging rather than being asked for again. Measured against a whole-page answer, the old reading gave 11 rows with a heading among them and the new one gives 10 rows with none. The rows are also read back inside a table rather than a detached table body, which is the one context where a browser is required to parse them as rows.
The column now stands 1.2rem narrower than the window, through a named property, the margins either side of the centered container are halved to 1.5rem, and an activity whose width is working room rather than a line of prose carries a class that drops the cap: the wiki page whether written or read, mail, and messages. Measured at 1920 pixels the column went from 1322 to 1901 and the wiki page from 885 to 1853 in both modes, with nothing overflowing at 360, 601, 800 or 1060 pixels. Every length this arc added is said in rem and every direction as a logical property, which is what Phase 5 settled the stylesheet on.
The crash line now names the account, the folder and the word after UID, and the periodic memory line says how many folder indexes are cached and how many records they hold, so the next occurrence answers both questions itself. Rebuilding a folder's index no longer grows its snapshot as one string nor reads the whole index a second time as records; it streams both files a line at a time and takes only flags from the index. Measured on a folder of 77,000 messages that is 2.0MB held beside the records against 6.2MB, but the same measurement shows neither figure explains a climb of 319MB, so the cause is not yet established.
listen, so
anything reading a setting before listening began found nothing there,
and the root index carried its own copy of the request-size default.
The settings now live in defaultServerGlobals alone.
The constructor starts a server with all of them rather than two,
listen starts from the same call, and the root index asks
for the one it wants. Checked against the source, all fifteen settings
read anywhere in the class are among them, bar one read with a fallback
of its own.
Separately, the file cache remembers the hash of what was last read from or written to each path, and a write whose contents match that hash is skipped, so saving something unchanged costs no disk write.
A session's contents now live in a file of their own, read back when a request needs them and written through the cache above, so a request that does not change a session costs no write at all. What the server holds for each is its time, its use-count and nothing else, which is all the sweep for stale sessions and the eviction of the least recently used ever needed; evicting one now takes its file with it. A session grown past what the server accepts as a request body is refused rather than written.
The block in listen that restored sessions from a
configuration array is gone: nothing produced what it read.
A storage class now reports a problem through a handler that can be handed in, falling back to the error log when none is. The test that makes an oversize message catches it, so a run says only what the tests themselves say, and the saying of it is checked rather than merely heard. Measured on a filtered run, standard error is empty and every line on standard output is a test result or a heading.
The cache now has a ceiling on the message records held across it and drops the least recently used until they fit, and a folder too large to fit on its own is not held at all rather than emptying the cache for something that still would not. The snapshot a rebuild would write is hashed a line at a time and the write is skipped when the file already holds it. Measured on an index of 154,853 lines and 77,427 messages, the file is 3.2MB, one read costs 0.352 seconds and the records it makes cost 48MB.
What the cache holds changed too: three lists of plain values beside each other, keyed by uid, rather than a small array for every message. A record holds four scalars and PHP spends more on the array around them than on the values inside, so the same folder costs 6.01MB where it cost 48MB, and building the records back out of it takes 0.035 seconds against 0.352 to parse the index file again. A first attempt at this packed the values into one binary string, which measured slower to decode than re-reading the file and was dropped; plain values keyed by uid need no unpacking at all. Counting live messages needs no records built, since the uids are the keys the compact form is already keyed by.